package model.entity.container;

import gui.inventory.ProductContainerData;

import java.util.*;

import model.entity.Entity;
import model.entity.Item;
import model.entity.Product;
import model.exceptions.InvalidNameException;
import model.management.ProductGroupManager;

import common.Result;

//@formatter:off
/**
 * Describes a product container, with methods allowing
 * addition and removal of <code>ProductGroup</code>s.
 * 
 * @author Ryan Cheatham
 * 
 * {@invariant getName() != null}
 * {@invariant getName().length() > 0}
 */
//@formatter:on
public abstract class ProductContainer extends Entity
{
  private static final long              serialVersionUID = -889994870870805724L;

  /** The list of groups this product container holds directly (not through child product groups) */
  protected List<ProductGroup>           productGroups    = new ArrayList<ProductGroup>();

  /** The name of this product container. */
  protected String                       name;

  /** The list of items this product container holds directly (not through child product groups) */
  protected Set<Item>                    items;

  protected Map<Product, Set<Item>>      itemsByProduct   = new HashMap<Product, Set<Item>>();

  /**
   * The list of products this product container holds directly (not through child product groups)
   */
  protected Set<Product>                 products;

  /**
   * The id of this product container in the database
   */
  protected int                          id;

  /**
   * The id of this product container's parent in the database
   */
  protected int                          parent_id;

  private transient ProductContainerData data;

  //@formatter:off
  /**
   * Constructs a new product container with the given name.
   * 
   * @param name The name of the product container
   * 
   * {@pre name != null}
   * {@pre name.length() > 0}
   * 
   * {@post getName() == name}
 * @throws InvalidNameException 
   */
  //@formatter:on
  public ProductContainer(String name) throws InvalidNameException
  {
    if (!isValidName(name))
    {
      throw new InvalidNameException("Name must contain at least one character.");
    }

    this.name = name;
    items = new HashSet<Item>();
    products = new HashSet<Product>();
  }

  /**
   * Returns the name of this product container
   */
  public String getName()
  {
    return name;
  }

  //@formatter:off
  /**
   * Sets the name of this product container to the given name.
   * 
   * @param name
   *          The new name.
   * 
   * {@pre name != null}
   * {@pre name.length() > 0}
   * 
   * {@post getName() == name}
 * @throws InvalidNameException 
   */
  //@formatter:on
  public void setName(String name) throws InvalidNameException
  {
    if (!isValidName(name))
    {
      throw new InvalidNameException("Name must contain at least one character.");
    }

    this.name = name;
  }

  /**
   * Returns an iterator through all the items in this container.
   */
  public Iterator<Item> getItems()
  {
    return Collections.unmodifiableSet(items).iterator();
  }

  /**
   * Gets all the items in this container of the given product, searching recursively through the
   * product container tree.
   * 
   * @param product
   *          the product to search for
   * @return a collection of all the items found
   */
  public Collection<Item> getAllItems(Product product)
  {
    Collection<Item> items = itemsByProduct.get(product);
    if (items == null)
      items = new HashSet<Item>();

    for (ProductGroup group : productGroups)
      items.addAll(group.getAllItems(product));

    return Collections.unmodifiableCollection(items);
  }

  /**
   * Recursively searches this container and all its children to determine if this container has
   * any items.
   * 
   * @return whether this container has any items.
   */
  public boolean hasItems()
  {
    if (!items.isEmpty())
      return true;

    for (ProductGroup group : productGroups)
    {
      if (group.hasItems())
        return true;
    }

    return false;
  }

  /**
   * Returns an iterator through all the items in this container of the given product.
   */
  public Iterator<Item> getItems(Product product)
  {
    Set<Item> items = itemsByProduct.get(product);

    if (items == null)
      return Collections.emptyIterator();

    return Collections.unmodifiableSet(items).iterator();
  }

  /**
   * Gets the number of items of the given product in this container.
   * 
   * @param product
   *          The product to search for
   * @return The number of items found
   */
  public int getItemCount(Product product)
  {
    Set<Item> items = itemsByProduct.get(product);
    if (items == null)
      return 0;

    return items.size();
  }

  /**
   * Gets the total number of items in this container.
   */
  public int getItemCount()
  {
    return items.size();
  }

  /**
   * Returns an iterator through all the product groups in this container.
   */
  public Iterator<ProductGroup> getProductGroups()
  {
    Collections.sort(productGroups);
    return Collections.unmodifiableList(productGroups).iterator();
  }

  /**
   * Returns the Storage Unit that contains this container
   */
  public abstract StorageUnit getStorageUnit();

  /**
   * Validates whether the given product group can be added to this product container.
   * 
   * @param group
   *          The product group to validate.
   * @return a result, whose status indicates whether the given product group can be added to this
   *         container.
   * 
   *         {@pre group != null}
   */
  public Result validateAddProductGroup(ProductGroup group)
  {
    Result result = ProductGroupManager.validateAddProductGroup(group);
    if (result.getStatus() == false)
      return result;

    for (ProductGroup subGroup : productGroups)
    {
      if (subGroup.getName().equals(group.getName()))
        return new Result(false, "Cannot add ProductGroup"
            + " - this StorageUnit already contains a ProductGroup with that name!");
    }

    return new Result(true);
  }

  //@formatter:off
  /**
   * Validates whether the given product group can be added to this product container.
   * If so, the group is added to this container.
   * 
   * @param group
   *          The group to be added to this container.
   * @return a result, indicating whether this operation was successful or not.
   * 
   * {@pre group != null}
   * 
   * {@post the group is contained in this container}
   */
  //@formatter:on
  public Result addProductGroup(ProductGroup group)
  {
    Result canAdd = validateAddProductGroup(group);
    if (!canAdd.getStatus())
    {
      return canAdd;
    }

    productGroups.add(group);
    ProductGroupManager.addProductGroup(group);

    return new Result(true);
  }

  /**
   * Attempts to remove the given product group from this container.
   * 
   * @param group
   *          The product group to remove.
   * @return a result, indicating whether this operation was successful or not.
   * 
   *         {@post this group no longer contains the given group}
   */
  public Result removeProductGroup(ProductGroup group)
  {
    productGroups.remove(group);
    return new Result(true);
  }

  //@formatter:off
  /**
   * Attempts to add the given item to this product container.
   * 
   * @param item
   *          The item to be added.
   * @return a result, indicating whether the given item can be
   *        added to this product container or not.
   * 
   * {@pre item != null}
   * 
   * {@post this container contains the given item}
   */
  //@formatter:on
  public Result addItem(Item item)
  {
    return addItems(new Item[]
    {
      item
    });
  }

  /**
   * Attempts to add the given items to this product container.
   * 
   * @param items
   *          the array of items to be added
   * @return a result, indicating whether the operation was successful.
   * 
   *         {@pre items != null}
   * 
   * @throws NullPointerException
   *           if items is null
   */
  public Result addItems(Item[] items)
  {
    if (items.length == 0)
      return new Result(true);

    Product product = items[0].getProduct();
    Set<Item> itemSet = itemsByProduct.get(product);
    if (itemSet == null)
    {
      itemSet = new HashSet<Item>();
      itemsByProduct.put(product, itemSet);
    }

    for (Item item : items)
    {
      this.items.add(item);
      itemSet.add(item);
    }

    return new Result(true);
  }

  public Result validateRemoveItem(Item item)
  {
    return new Result(true);
  }

  /**
   * Attempts to remove the given item from this product container.
   * 
   * @param item
   *          The item to remove.
   * @return a result, indicating whether this operation was successful or not.
   * 
   *         {@post this container no longer contains the given item}
   */
  public Result removeItem(Item item)
  {
    items.remove(item);

    Set<Item> itemSet = itemsByProduct.get(item.getProduct());
    if (itemSet != null)
      itemSet.remove(item);

    for (ProductGroup group : productGroups)
      group.removeItem(item);

    return new Result(true);
  }

  /**
   * Validates whether the given product can be added directly to this product container.
   * Specifically, this method checks to see if this product is held by any child containers. If
   * so, then the product cannot be added to this container directly.
   * 
   * @param product
   *          The product to validate.
   * @return a result, indicating whether the given product can be added directly to this product
   *         container.
   * 
   *         {@pre product != null}
   */
  public Result validateAddProduct(Product product)
  {
    return new Result(true);
  }

  //@formatter:off
  /**
   * Attempts to add the given product directly to this product container.
   * 
   * @param product
   *          The product to add.
   * @return a result, indicating whether this operation was successful or not.
   * 
   * {@pre product != null}
   * 
   * {@post this container contains the given product}
   */
  //@formatter:on
  public Result addProduct(Product product)
  {
    Result result = validateAddProduct(product);
    if (result.getStatus() == false)
      return result;

    products.add(product);

    return new Result(true);
  }

  public Result validateRemoveProduct(Product product)
  {
    return new Result(true);
  }

  //@formatter:off
  /**
   * Attempts to remove the given product from this product container specifically.
   * If this product container does not contain the product,
   * but a child container does, then the product is not removed.
   * 
   * @param product
   *          The product to remove.
   * @return a result, indicating whether this operation was successful or not.
   * 
   * {@pre product != null}
   * 
   * {@post this container no longer contains the given product}
   */
  //@formatter:on
  public Result removeProduct(Product product)
  {
    boolean removed = products.remove(product);
    removed &= (itemsByProduct.remove(product) != null);

    return new Result(removed);
  }

  /**
   * <p>
   * Gets the product container that contains the given product within this container's product
   * container tree.
   * </p>
   * <p>
   * In other words, if this container contains the given product, this container is returned. If a
   * child container contains the given product, that container is returned. If neither this nor a
   * child container contains the given product, <code>null</code> is returned.
   * </p>
   * 
   * @param product
   *          The product whose container to search for in this container tree.
   * @return the product container that contains the given product, or <code>null</code> if there
   *         is none.
   */
  public ProductContainer getProductContainer(Product product)
  {
    if (products.contains(product))
      return this;

    for (ProductGroup group : productGroups)
    {
      ProductContainer container = group.getProductContainer(product);
      if (container != null)
      {
        return container;
      }
    }

    return null;
  }

  public static boolean isValidName(String name)
  {
    return name != null && name.length() > 0;
  }

  public boolean equals(ProductContainerData obj)
  {
    if (!name.equals(obj.getName()))
      return false;

    return equals(obj.getTag());
  }

  public boolean containsProduct(Product product)
  {
    return products.contains(product);
  }

  public boolean containsProductGroup(ProductGroup group)
  {
    return productGroups.contains(group);
  }

  @Override
  public int hashCode()
  {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((name == null) ? 0 : name.hashCode());
    return result;
  }

  @Override
  public boolean equals(Object obj)
  {
    if (this == obj)
      return true;
    if (obj == null)
      return false;
    if (!(obj instanceof ProductContainer))
      return false;
    ProductContainer other = (ProductContainer) obj;
    if (name == null)
    {
      if (other.name != null)
        return false;
    }
    else if (!name.equals(other.name))
      return false;
    return true;
  }

  public ProductContainerData generateProductContainerData()
  {
    if (data == null)
    {
      data = new ProductContainerData(name);
      data.setTag(this);
    }
    return data;
  }

  public Iterator<Product> getProducts()
  {
    return Collections.unmodifiableSet(products).iterator();
  }

  public int getId()
  {
    return id;
  }

  public void setId(int id)
  {
    this.id = id;
  }

  public int getParent_id()
  {
    return parent_id;
  }

  public void setParent_id(int parent_id)
  {
    this.parent_id = parent_id;
  }
}
